<?php

/**
 *  PayPal IPN Listener
 *
 *  A class to listen for and handle Instant Payment Notifications (IPN) from
 *  the PayPal server.
 *
 *  https://github.com/Quixotix/PHP-PayPal-IPN
 *
 * @package        PHP-PayPal-IPN
 * @author         Micah Carrick
 * @copyright  (c) 2012 - Micah Carrick
 * @version        2.1.0
 */
class IpnListener {

	/**
	 *  If true, the recommended cURL PHP library is used to send the post back
	 *  to PayPal. If flase then fsockopen() is used. Default true.
	 *
	 * @var boolean
	 */
	public $use_curl = true;

	/**
	 *  If true, explicitly sets cURL to use SSL version 3. Use this if cURL
	 *  is compiled with GnuTLS SSL.
	 *
	 * @var boolean
	 */
	public $force_ssl_v3 = true;

	/**
	 *  If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any
	 *  "Location: ..." headers in the response.
	 *
	 * @var boolean
	 */
	public $follow_location = false;

	/**
	 *  If true, an SSL secure connection (port 443) is used for the post back
	 *  as recommended by PayPal. If false, a standard HTTP (port 80) connection
	 *  is used. Default true.
	 *
	 * @var boolean
	 */
	public $use_ssl = true;

	/**
	 *  If true, the paypal sandbox URI www.sandbox.paypal.com is used for the
	 *  post back. If false, the live URI www.paypal.com is used. Default false.
	 *
	 * @var boolean
	 */
	public $use_sandbox = false;

	/**
	 *  The amount of time, in seconds, to wait for the PayPal server to respond
	 *  before timing out. Default 30 seconds.
	 *
	 * @var int
	 */
	public $timeout = 30;

	private $post_data = array();
	private $post_uri = '';
	private $response_status = '';
	private $response = '';

	const PAYPAL_HOST  = 'www.paypal.com';
	const SANDBOX_HOST = 'www.sandbox.paypal.com';

	/**
	 *  Post Back Using cURL
	 *
	 *  Sends the post back to PayPal using the cURL library. Called by
	 *  the processIpn() method if the use_curl property is true. Throws an
	 *  exception if the post fails. Populates the response, response_status,
	 *  and post_uri properties on success.
	 *
	 * @param  string  The post data as a URL encoded string
	 */
	protected function curlPost( $encoded_data ) {

		if ( $this->use_ssl ) {
			$uri            = 'https://' . $this->getPaypalHost() . '/cgi-bin/webscr';
			$this->post_uri = $uri;
		} else {
			$uri            = 'http://' . $this->getPaypalHost() . '/cgi-bin/webscr';
			$this->post_uri = $uri;
		}

		$ch = curl_init();

		curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
		curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
		curl_setopt( $ch, CURLOPT_CAINFO,
			dirname( __FILE__ ) . "/cert/api_cert_chain.crt" );
		curl_setopt( $ch, CURLOPT_URL, $uri );
		curl_setopt( $ch, CURLOPT_POST, true );
		curl_setopt( $ch, CURLOPT_POSTFIELDS, $encoded_data );
		curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, $this->follow_location );
		curl_setopt( $ch, CURLOPT_TIMEOUT, $this->timeout );
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
		curl_setopt( $ch, CURLOPT_HEADER, true );

		if ( $this->force_ssl_v3 ) {
			curl_setopt( $ch, CURLOPT_SSLVERSION, 3 );
		}

		$this->response        = curl_exec( $ch );
		$this->response_status = strval( curl_getinfo( $ch, CURLINFO_HTTP_CODE ) );

		if ( $this->response === false || $this->response_status == '0' ) {
			$errno  = curl_errno( $ch );
			$errstr = curl_error( $ch );
			throw new Exception( "cURL error: [$errno] $errstr" );
		}
	}

	/**
	 *  Post Back Using fsockopen()
	 *
	 *  Sends the post back to PayPal using the fsockopen() function. Called by
	 *  the processIpn() method if the use_curl property is false. Throws an
	 *  exception if the post fails. Populates the response, response_status,
	 *  and post_uri properties on success.
	 *
	 * @param  string  The post data as a URL encoded string
	 */
	protected function fsockPost( $encoded_data ) {

		if ( $this->use_ssl ) {
			$uri            = 'ssl://' . $this->getPaypalHost();
			$port           = '443';
			$this->post_uri = $uri . '/cgi-bin/webscr';
		} else {
			$uri            = $this->getPaypalHost(); // no "http://" in call to fsockopen()
			$port           = '80';
			$this->post_uri = 'http://' . $uri . '/cgi-bin/webscr';
		}

		$fp = fsockopen( $uri, $port, $errno, $errstr, $this->timeout );

		if ( ! $fp ) {
			// fsockopen error
			throw new Exception( "fsockopen error: [$errno] $errstr" );
		}

		$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
		$header .= "Host: " . $this->getPaypalHost() . "\r\n";
		$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
		$header .= "Content-Length: " . strlen( $encoded_data ) . "\r\n";
		$header .= "Connection: Close\r\n\r\n";

		fputs( $fp, $header . $encoded_data . "\r\n\r\n" );

		while ( ! feof( $fp ) ) {
			if ( empty( $this->response ) ) {
				// extract HTTP status from first line
				$this->response .= $status = fgets( $fp, 1024 );
				$this->response_status = trim( substr( $status, 9, 4 ) );
			} else {
				$this->response .= fgets( $fp, 1024 );
			}
		}

		fclose( $fp );
	}

	private function getPaypalHost() {
		if ( $this->use_sandbox ) {
			return self::SANDBOX_HOST;
		} else {
			return self::PAYPAL_HOST;
		}
	}

	/**
	 *  Get POST URI
	 *
	 *  Returns the URI that was used to send the post back to PayPal. This can
	 *  be useful for troubleshooting connection problems. The default URI
	 *  would be "ssl://www.sandbox.paypal.com:443/cgi-bin/webscr"
	 *
	 * @return string
	 */
	public function getPostUri() {
		return $this->post_uri;
	}

	/**
	 *  Get Response
	 *
	 *  Returns the entire response from PayPal as a string including all the
	 *  HTTP headers.
	 *
	 * @return string
	 */
	public function getResponse() {
		return $this->response;
	}

	/**
	 *  Get Response Status
	 *
	 *  Returns the HTTP response status code from PayPal. This should be "200"
	 *  if the post back was successful.
	 *
	 * @return string
	 */
	public function getResponseStatus() {
		return $this->response_status;
	}

	/**
	 *  Get Text Report
	 *
	 *  Returns a report of the IPN transaction in plain text format. This is
	 *  useful in emails to order processors and system administrators. Override
	 *  this method in your own class to customize the report.
	 *
	 * @return string
	 */
	public function getTextReport() {

		$r = '';

		// date and POST url
		for ( $i = 0; $i < 80; $i ++ ) {
			$r .= '-';
		}
		$r .= "\n[" . date( 'm/d/Y g:i A' ) . '] - ' . $this->getPostUri();
		if ( $this->use_curl ) {
			$r .= " (curl)\n";
		} else {
			$r .= " (fsockopen)\n";
		}

		// HTTP Response
		for ( $i = 0; $i < 80; $i ++ ) {
			$r .= '-';
		}
		$r .= "\n{$this->getResponse()}\n";

		// POST vars
		for ( $i = 0; $i < 80; $i ++ ) {
			$r .= '-';
		}
		$r .= "\n";

		foreach ( $this->post_data as $key => $value ) {
			$r .= str_pad( $key, 25 ) . "$value\n";
		}
		$r .= "\n\n";

		return $r;
	}

	/**
	 *  Process IPN
	 *
	 *  Handles the IPN post back to PayPal and parsing the response. Call this
	 *  method from your IPN listener script. Returns true if the response came
	 *  back as "VERIFIED", false if the response came back "INVALID", and
	 *  throws an exception if there is an error.
	 *
	 * @param array
	 *
	 * @return boolean
	 */
	public function processIpn( $post_data = null ) {

		$encoded_data = 'cmd=_notify-validate';

		if ( $post_data === null ) {
			// use raw POST data
			if ( ! empty( $_POST ) ) {
				$this->post_data = $_POST;
				$encoded_data .= '&' . file_get_contents( 'php://input' );
			} else {
				throw new Exception( "No POST data found." );
			}
		} else {
			// use provided data array
			$this->post_data = $post_data;

			foreach ( $this->post_data as $key => $value ) {
				$encoded_data .= "&$key=" . urlencode( $value );
			}
		}

		if ( $this->use_curl ) {
			$this->curlPost( $encoded_data );
		} else {
			$this->fsockPost( $encoded_data );
		}

		if ( strpos( $this->response_status, '200' ) === false ) {
			throw new Exception( "Invalid response status: " . $this->response_status );
		}

		if ( strpos( $this->response, "VERIFIED" ) !== false ) {
			return true;
		} elseif ( strpos( $this->response, "INVALID" ) !== false ) {
			return false;
		} else {
			throw new Exception( "Unexpected response from PayPal." );
		}
	}

	/**
	 *  Require Post Method
	 *
	 *  Throws an exception and sets a HTTP 405 response header if the request
	 *  method was not POST.
	 */
	public function requirePostMethod() {
		// require POST requests
		if ( $_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST' ) {
			header( 'Allow: POST', true, 405 );
			throw new Exception( "Invalid HTTP request method." );
		}
	}
}

